package net.brucecooper.mindmapgadget.client;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import net.brucecooper.mindmap.client.AbstractNodeDataStore;
import net.brucecooper.mindmap.client.Node;
import net.brucecooper.mindmap.client.NodeDataStoreEvent;
import net.brucecooper.mindmap.client.NodeOrientation;
import net.brucecooper.mindmap.client.NodeOutlineStyle;

import org.cobogw.gwt.waveapi.gadget.client.State;
import org.cobogw.gwt.waveapi.gadget.client.StateUpdateEvent;
import org.cobogw.gwt.waveapi.gadget.client.StateUpdateEventHandler;
import org.cobogw.gwt.waveapi.gadget.client.WaveFeature;

import com.google.gwt.core.client.GWT;
import com.google.gwt.json.client.JSONArray;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONString;
import com.google.gwt.json.client.JSONValue;
import com.google.gwt.user.client.Window;
import com.google.gwt.xml.client.Document;
import com.google.gwt.xml.client.Element;
import com.google.gwt.xml.client.NodeList;

public class WaveNodeStore extends AbstractNodeDataStore {
    private static final String NAME_SUFFIX = "name";
    private static final String ICON_SUFFIX = "icon";
    private static final String NODE_PREFIX = "node";
    private static final String VOTESUP_SUFFIX = "votesUp";
    private static final String VOTESDOWN_SUFFIX = "votesDown";
    private static final String CHILDREN_SUFFIX = "children";
    private static final String NODE_COUNTER_PARAM = "nodeCounter";
    private static final String COLLAPSED_SUFFIX = "collapsedInitially";
    private static final String FOREGROUND_SUFFIX = "fg";
    private static final String BACKGROUND_SUFFIX = "bg";
    private static final String OUTLINE_SUFFIX = "outline";
    
    private static final String ROOT_NODE_ORIENTATION = "orientation";

    private WaveFeature wave;

    HashMap<String, String> deltas = new HashMap<String, String>();
    HashMap<Integer, Node> existingState = new HashMap<Integer, Node>();
    private NodeOrientation rootNodeOrientation = null;
    private boolean hasReceivedData;

    WaveNodeStore(WaveFeature wave) {
        this.wave = wave;
        existingState.put(0, new Node(0, "loading...", new ArrayList<Integer>()));
        rootNodeOrientation = NodeOrientation.RIGHT;

        wave.addStateUpdateEventHandler(new StateUpdateEventHandler() {
            public void onUpdate(StateUpdateEvent event) {
                syncState(event.getState());
                hasReceivedData = true;
            }
        });
    }

    private void syncState(State state, int node, Set<Integer> nodesVisited, List<NodeDataStoreEvent> nameChanges,
            List<NodeDataStoreEvent> insertions, List<NodeDataStoreEvent> deletions) {
        
        Node newNode = fetchNode(node);
        Node existing = existingState.get(node);

        if (existing != null) {
            if (!existing.equals(newNode)) {
                nameChanges.add(new NodeDataStoreEvent(this, node));
            }

            // Now check its kids.
            List<Integer> old = new ArrayList<Integer>(existing.children);
            newNode.children.remove(new Integer(node));
            // doLog("Existing children is " + old);
            for (Integer newID : newNode.children) {
                if (!old.remove(newID)) {
                    // Wasn't in the old one, so must be new
                    insertions.add(new NodeDataStoreEvent(this, node, newID));

                    // We need to populate the model with that node's children
                    populateExisting(newID);
                } else {
                    // Same old as new, recurse into it.
                    syncState(state, newID, nodesVisited, nameChanges, insertions, deletions);

                }
            }

            // Anything left in old is a deletion
            for (Integer oldID : old) {
                deletions.add(new NodeDataStoreEvent(this, node, oldID));
                // Need to purge children and all of its children from the
                // existing state, unless it has already been placed into the
                // "Adds" because of a move operation
                if (!listContainsChildID(insertions, oldID)) {
                    deleteFromExisting(oldID);
                }
            }
            // Make sure the existing model is updated correctly.
            // doLog("Updating children of node " + node + " to be " +
            // children);
            existingState.put(node, newNode);
        } else {
            // It doesn't exist.... Err? This shouldn't happen
        }
    }

    private boolean listContainsChildID(List<NodeDataStoreEvent> eventList, int childID) {
        for (NodeDataStoreEvent e : eventList) {
            if (e.getChildNode() == childID)
                return true;
        }
        return false;
    }

    private void deleteFromExisting(Integer oldID) {
//        doLog("Deleting " + oldID);
        Node node = existingState.remove(oldID);
        if (node != null) {
//            doLog("Children are " + node.children);
            for (Integer childID : node.children) {
                deleteFromExisting(childID);
            }
        }
    }

    private void populateExisting(int newID) {
//        doLog("Populating " + newID);
        Node node = fetchNode(newID);
        if (node != null) {
            existingState.put(newID, node);

            node.children.remove(new Integer(newID));
            for (int childID : node.children) {
                populateExisting(childID);
            }
        }
    }

    private Node fetchNode(int id) {
        State state = wave.getState();

        if (state == null)
            return null;
        String name = state.get(NODE_PREFIX + id + NAME_SUFFIX);
        String icon = state.get(NODE_PREFIX + id + ICON_SUFFIX);
        String votesUpStr = state.get(NODE_PREFIX + id + VOTESUP_SUFFIX);
        String votesDownStr = state.get(NODE_PREFIX + id + VOTESDOWN_SUFFIX);
        boolean collapsedInitially = "true".equalsIgnoreCase(state.get(NODE_PREFIX + id + COLLAPSED_SUFFIX));
        String foreground = state.get(NODE_PREFIX + id + FOREGROUND_SUFFIX);
        String background = state.get(NODE_PREFIX + id + BACKGROUND_SUFFIX);
        String outlineStr = state.get(NODE_PREFIX + id + OUTLINE_SUFFIX);
        
        ArrayList<Integer> children = new ArrayList<Integer>();
        String childrenStr = state.get(NODE_PREFIX + id + CHILDREN_SUFFIX);
        String[] childrenArray;
        if (childrenStr == null) {
            childrenArray = new String[0];
        } else {
            childrenArray = childrenStr.substring(1, childrenStr.length() - 1).split(", ");
        }

        for (String child : childrenArray) {
            children.add(Integer.parseInt(child));
        }
        ArrayList<String> votesUp = null;
        if (votesUpStr != null) {
            votesUp = new ArrayList<String>();
            String[] votesUpArray = votesUpStr.substring(1, votesUpStr.length() - 1).split(", ");
            for (String child : votesUpArray) {
                votesUp.add(child);
            }
        }
        ArrayList<String> votesDown = null;
        if (votesDownStr != null) {
            votesDown = new ArrayList<String>();
            String[] votesDownArray = votesDownStr.substring(1, votesDownStr.length() - 1).split(", ");
            for (String child : votesDownArray) {
                votesDown.add(child);
            }
        }

        Node n = new Node(id, name, children);
        n.icon = icon;
        n.votesUp = votesUp;
        n.votesDown = votesDown;
        n.collapsedInitially = collapsedInitially;
        n.foreground = foreground == null ? "000000" : foreground;
        n.background = background == null ? "ffffff" : background;
        
        if (outlineStr == null) {
            n.outline = id == ROOT_NODE ? NodeOutlineStyle.CIRCLE : NodeOutlineStyle.UNDERLINE;
        } else {
            try {
                n.outline = NodeOutlineStyle.valueOf(outlineStr);
            } catch (IllegalArgumentException ex) {
                GWT.log("Error setting node outline style for node " + id + " to " + outlineStr, ex);
                n.outline = NodeOutlineStyle.UNDERLINE;
            }
        }
       
        return n;
    }

    private void syncState(State state) {
        List<NodeDataStoreEvent> nameChanges = new ArrayList<NodeDataStoreEvent>();
        List<NodeDataStoreEvent> insertions = new ArrayList<NodeDataStoreEvent>();
        List<NodeDataStoreEvent> deletions = new ArrayList<NodeDataStoreEvent>();
        
        

        if (state.get(NODE_PREFIX + 0 + NAME_SUFFIX) == null) {
            // Its a brand new model.
            addChildrenDelta(0, new ArrayList<Integer>());
            addNameDelta(0, "Root Node");
            submitDeltas();
        } else {
            NodeOrientation readNodeOrientation;
            String orientationStr = wave.getState().get(ROOT_NODE_ORIENTATION);
            try {
                readNodeOrientation = orientationStr == null ? NodeOrientation.RIGHT : NodeOrientation.valueOf(orientationStr);
            } catch (IllegalArgumentException ex) {
                GWT.log("Error parsing " + orientationStr + " as an orientation", ex);
                readNodeOrientation = NodeOrientation.RIGHT;
            }
            
            
            Set<Integer> nodesVisited = new HashSet<Integer>();
            syncState(state, 0, nodesVisited, nameChanges, insertions, deletions);

            if (readNodeOrientation != rootNodeOrientation) {
                rootNodeOrientation = readNodeOrientation;
                fireModelChanged();
            } else {
                for (NodeDataStoreEvent e : insertions) {
                    fireNodeAdded(e);
                }
                for (NodeDataStoreEvent e : deletions) {
                    fireNodeDeleted(e);
                }
                for (NodeDataStoreEvent e : nameChanges) {
                    fireNodeChanged(e);
                }
            }
        }

    }


    public int addChildTo(int parent) {

        State state = wave.getState();
        String counterStr = state.get(NODE_COUNTER_PARAM);
        int counter;
        if (counterStr == null) {
            counter = 0;
        } else {
            counter = Integer.parseInt(counterStr);
        }
        counter++;

        deltas.put(NODE_COUNTER_PARAM, Integer.toString(counter));
        ArrayList<Integer> pChildren = new ArrayList<Integer>(getNodeChildren(parent));
        pChildren.add(counter);

        ArrayList<Integer> newChildren = new ArrayList<Integer>();
        newChildren.add(counter);

        addChildrenDelta(parent, pChildren);
        addNameDelta(counter, "New Node");
        addChildrenDelta(counter, new ArrayList<Integer>());

        submitDeltas();

        return counter;
    }

    public String getNodeName(int id) {
        Node node = existingState.get(id);
        return node == null ? "Unavailable" : node.name;
    }

    public ArrayList<Integer> getNodeChildren(int id) {
        Node node = existingState.get(id);
        return node == null ? new ArrayList<Integer>() : node.children;
    }

    public void removeNodeFrom(int parentNode, int id) {
        ArrayList<Integer> children = getNodeChildren(parentNode);
        children.remove(new Integer(id));

        addChildrenDelta(parentNode, children);
        addDeleteDelta(id);
        submitDeltas();
    }

    public void setNodeName(int nodeID, String newName) {
        addNameDelta(nodeID, newName);
        submitDeltas();
    }

    private void addChildrenDelta(int nodeID, ArrayList<Integer> children) {
        deltas.put(NODE_PREFIX + nodeID + CHILDREN_SUFFIX, children.toString());
    }

    private void addNameDelta(int nodeID, String newName) {
        deltas.put(NODE_PREFIX + nodeID + NAME_SUFFIX, newName);
    }
    private void addCollapsedInitiallyDelta(int nodeID, boolean  collapse) {
        deltas.put(NODE_PREFIX + nodeID + COLLAPSED_SUFFIX, collapse ? "true" : null);
    }
    private void addForegroundDelta(int nodeID, String  value) {
        deltas.put(NODE_PREFIX + nodeID + FOREGROUND_SUFFIX, value);
    }
    private void addBackgroundDelta(int nodeID, String  value) {
        deltas.put(NODE_PREFIX + nodeID + BACKGROUND_SUFFIX, value);
    }
    private void addOutlineDelta(int nodeID, NodeOutlineStyle value) {
        deltas.put(NODE_PREFIX + nodeID + OUTLINE_SUFFIX, value.toString());
    }

    private void addIconDelta(int nodeID, String newIcon) {
        deltas.put(NODE_PREFIX + nodeID + ICON_SUFFIX, newIcon);
    }

    private void addVotesUpDelta(int nodeID, ArrayList<String> votes) {
        deltas.put(NODE_PREFIX + nodeID + VOTESUP_SUFFIX, votes.size() == 0 ? null : votes.toString());
    }

    private void addVotesDownDelta(int nodeID, ArrayList<String> votes) {
        deltas.put(NODE_PREFIX + nodeID + VOTESDOWN_SUFFIX, votes.size() == 0 ? null : votes.toString());
    }
    private void addRootNodeOrientationDelta(NodeOrientation orientation) {
        deltas.put(ROOT_NODE_ORIENTATION, orientation.toString());
        
    }

    private void addDeleteDelta(int id) {
        // Delete all children
        for (int childID : getNodeChildren(id)) {
            addDeleteDelta(childID);
        }
        // Setting properties to null is a way of deleting them
        deltas.put(NODE_PREFIX + id + NAME_SUFFIX, null);
        deltas.put(NODE_PREFIX + id + CHILDREN_SUFFIX, null);
        deltas.put(NODE_PREFIX + id + ICON_SUFFIX, null);
        deltas.put(NODE_PREFIX + id + VOTESUP_SUFFIX, null);
        deltas.put(NODE_PREFIX + id + VOTESDOWN_SUFFIX, null);
        deltas.put(NODE_PREFIX + id + COLLAPSED_SUFFIX, null);
        deltas.put(NODE_PREFIX + id + FOREGROUND_SUFFIX, null);
        deltas.put(NODE_PREFIX + id + BACKGROUND_SUFFIX, null);
        deltas.put(NODE_PREFIX + id + OUTLINE_SUFFIX, null);
    }

    private void submitDeltas() {
        if (wave != null) {
            // doLog("Submitting deltas of "+ deltas + ", existingState is " +
            // existingState);
            State state = wave.getState();

            if (state != null) {
                state.submitDelta(deltas);
                deltas.clear();
            }
        }
    }

    private void addClearDelta() {
        for (int id : existingState.keySet()) {
            deltas.put(NODE_PREFIX + id + NAME_SUFFIX, null);
            deltas.put(NODE_PREFIX + id + CHILDREN_SUFFIX, null);
            deltas.put(NODE_PREFIX + id + ICON_SUFFIX, null);
            deltas.put(NODE_PREFIX + id + VOTESUP_SUFFIX, null);
            deltas.put(NODE_PREFIX + id + VOTESDOWN_SUFFIX, null);
        }
    }

    public void replaceModel(Document newModel) {
        addClearDelta();

        int finalID = populateModel((Element)newModel.getElementsByTagName("node").item(0), ROOT_NODE);
        deltas.put(NODE_COUNTER_PARAM, Integer.toString(finalID));

        submitDeltas();
    }

    private int populateModel(Element e, int id) {
        int myID = id;
        
        NodeList children = e.getElementsByTagName("node");
        ArrayList<Integer> childIDs = new ArrayList<Integer>();

        if (children != null) {
            for (int i = 0; i < children.getLength(); i++) {
                Element childObj = (Element) children.item(i);
                int newID = ++id;
                id = populateModel(childObj, id);
                childIDs.add(newID);
            }
        }
        String name = e.getAttribute("TEXT");
        addNameDelta(myID, name);
        addChildrenDelta(myID, childIDs);
        
        NodeList iconElem = e.getElementsByTagName("icon");
        if (iconElem != null && iconElem.getLength() > 0) {
            String builtin = ((Element)iconElem.item(0)).getAttribute("BUILTIN");
            addIconDelta(myID, builtin);
        } else {
            addIconDelta(myID, null);
        }

        // All these components are left blank at this stage
        // TODO populate foreground, background, outline from model.
        deltas.put(NODE_PREFIX + id + VOTESUP_SUFFIX, null);
        deltas.put(NODE_PREFIX + id + VOTESDOWN_SUFFIX, null);
        deltas.put(NODE_PREFIX + id + COLLAPSED_SUFFIX, null);
        deltas.put(NODE_PREFIX + id + FOREGROUND_SUFFIX, null);
        deltas.put(NODE_PREFIX + id + BACKGROUND_SUFFIX, null);
        deltas.put(NODE_PREFIX + id + OUTLINE_SUFFIX, null);

        

        return id;
    }

    public String getNodeIcon(int id) {
        Node node = existingState.get(id);
        return node == null ? null : node.icon;
    }

    public void setIcon(int nodeID, String newIcon) {
        addIconDelta(nodeID, newIcon);
        submitDeltas();

    }

    public void changeNodeParent(int nodeID, int oldParent, int newParent) {
        ArrayList<Integer> oldChildren = new ArrayList<Integer>(getNodeChildren(oldParent));
        ArrayList<Integer> newChildren = new ArrayList<Integer>(getNodeChildren(newParent));

        oldChildren.remove(new Integer(nodeID));
        if (oldParent == newParent) {
            // If the old and new parent are the same, then we have two copies of the array above.
            // We need to remove it from both, before we re-add it.
            newChildren.remove(new Integer(nodeID));
            
        }
        newChildren.add(new Integer(nodeID));

        addChildrenDelta(oldParent, oldChildren);
        addChildrenDelta(newParent, newChildren);
        submitDeltas();
    }

    public void voteForNode(int nodeID, String username, boolean voteUp) {
        ArrayList<String> votesUp = (ArrayList<String>) getVotesUp(nodeID);
        if (votesUp != null) {
            votesUp = new ArrayList<String>(votesUp);
        }
        ArrayList<String> votesDown = (ArrayList<String>) getVotesDown(nodeID);
        if (votesDown != null) {
            votesDown = new ArrayList<String>(votesDown);
        }

        if (voteUp) {
            if (votesDown != null) {
                votesDown.remove(username);
                addVotesDownDelta(nodeID, votesDown);
            }
            if (votesUp == null) {
                votesUp = new ArrayList<String>();
            }
            if (!votesUp.contains(username)) {
                votesUp.add(username);
                addVotesUpDelta(nodeID, votesUp);
            }
        } else {
            if (votesUp != null) {
                votesUp.remove(username);
                addVotesUpDelta(nodeID, votesUp);
            }
            if (votesDown == null) {
                votesDown = new ArrayList<String>();
            }
            if (!votesDown.contains(username)) {
                votesDown.add(username);
                addVotesDownDelta(nodeID, votesDown);
            }
        }
        submitDeltas();
    }

    public List<String> getVotesDown(int nodeID) {
        Node node = existingState.get(nodeID);
        return node == null ? null : node.votesDown;
    }

    public List<String> getVotesUp(int nodeID) {
        Node node = existingState.get(nodeID);
        return node == null ? null : node.votesUp;
    }
    
    
    public boolean isReady() {
        return hasReceivedData;
    }

    public NodeOutlineStyle getNodeOutline(int nodeID) {
        Node node = existingState.get(nodeID);
        return node == null ? NodeOutlineStyle.CIRCLE : node.outline;
    }


    public boolean isNodeCollapsedInitially(int nodeID) {
        Node node = existingState.get(nodeID);
        return node == null ? null : node.collapsedInitially;
    }

    public void setNodeCollapsedInitially(int nodeID, boolean collapsed) {
        addCollapsedInitiallyDelta(nodeID, collapsed);
        submitDeltas();
    }

    public String getNodeBackground(int id) {
        Node node = existingState.get(id);
        return node == null ? null : node.background;
    }

    public String getNodeForeground(int id) {
        Node node = existingState.get(id);
        return node == null ? null : node.foreground;
    }

    public NodeOrientation getRootNodeOrientation() {
        return rootNodeOrientation;
    }

    public void setNodeBackground(int id, String rgb) {
        addBackgroundDelta(id, rgb);
        submitDeltas();
    }

    public void setNodeForeground(int id, String rgb) {
        addForegroundDelta(id, rgb);
        submitDeltas();
    }

    public void setNodeOutline(int nodeID, NodeOutlineStyle style) {
        addOutlineDelta(nodeID, style);
        submitDeltas();
        
    }

    public void setRootNodeOrientation(NodeOrientation orientation) {
        addRootNodeOrientationDelta(orientation);
        submitDeltas();
    }


    
}
